/*
arduino2tmsg_auto

Licensed under the MIT License <http://opensource.org/licenses/MIT>.
SPDX-License-Identifier: MIT
Copyright (c) 2018 zjzengdongyang <http://zjzdy.cn>.

Permission is hereby  granted, free of charge, to any  person obtaining a copy
of this software and associated  documentation files (the "Software"), to deal
in the Software  without restriction, including without  limitation the rights
to  use, copy,  modify, merge,  publish, distribute,  sublicense, and/or  sell
copies  of  the Software,  and  to  permit persons  to  whom  the Software  is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE  IS PROVIDED "AS  IS", WITHOUT WARRANTY  OF ANY KIND,  EXPRESS OR
IMPLIED,  INCLUDING BUT  NOT  LIMITED TO  THE  WARRANTIES OF  MERCHANTABILITY,
FITNESS FOR  A PARTICULAR PURPOSE AND  NONINFRINGEMENT. IN NO EVENT  SHALL THE
AUTHORS  OR COPYRIGHT  HOLDERS  BE  LIABLE FOR  ANY  CLAIM,  DAMAGES OR  OTHER
LIABILITY, WHETHER IN AN ACTION OF  CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE  OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

#include <iostream>
#include <streambuf>
#include <fstream>
#include <thread>
#include <future>
#include <list>
#include <map>
#include <chrono>
#include <cstring>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <json.hpp>
#define ASIO_STANDALONE 1 //纯asio库, 不引入boost
#include <asio.hpp>
#ifdef _WIN32
#ifndef _MBCS
#define _MBCS
#endif
#include <windows.h>
#include <tchar.h>
#else
#include <dirent.h>
#endif

using namespace std;
using namespace asio;
using namespace nlohmann;
using namespace std;
using namespace std::chrono;

//同步超时读取
struct readTimeoutStatus {
    bool isFinish;//该次读取是否完成
    size_t bytes_transferred;//传输的数据大小
};
/**
 * 读取数据直到缓冲区满,或超时,或出现错误
 * @param sp 串口
 * @param buf 缓冲区
 * @param wait_time 超时时间
 * @return 读取到的数据大小
 */
template<typename Rep, typename Period>
size_t readTimeout(serial_port &sp, mutable_buffer &buf, const std::chrono::duration<Rep, Period>& wait_time) {
    readTimeoutStatus ts = {false,0};//设置状态
    auto start = steady_clock::now();//记录当前时间
    async_read(sp, buf, [&ts] (error_code ec, size_t bytes_transferred) { ts = {true,bytes_transferred}; });//读取数据
    sp.get_io_service().reset();//重置io_service对象，为随后的run调用做准备
    while(!ts.isFinish && (steady_clock::now()-start < wait_time)) {
        auto newWaitTime = (milliseconds(10) > (wait_time-(steady_clock::now()-start)) ? milliseconds(10) : (wait_time-(steady_clock::now()-start)));//计算新的超时时间,最小10ms
        sp.get_io_service().run_one_for(newWaitTime);//让异步读取正常运行,
    }
    if(!ts.isFinish) {
        sp.cancel();//取消异步读取
        sp.get_io_service().run_one();//让其做可能存在的收尾工作
    }
    this_thread::yield();//让步给可能存在的其他线程
    return ts.bytes_transferred;
}

/**
 * 读取数据直到遇到停止符,或超时,或出现错误
 * 缓冲区内在停止符后仍然可能存在已读取的数据,请处理完本次数据后继续使用该缓冲区以读取后面的数据,否则后面的已读取的数据则被丢弃
 * @param sp 串口
 * @param buf 流缓冲区
 * @param delim 停止符
 * @param wait_time 超时时间
 * @return 读取到的数据大小,包括停止符在内
 */
template<typename Rep, typename Period>
size_t readTimeout_until(serial_port &sp, asio::streambuf &buf, char delim, const std::chrono::duration<Rep, Period>& wait_time) {
    readTimeoutStatus ts = {false,0};
    auto start = steady_clock::now();
    async_read_until(sp, buf, delim, [&ts] (error_code ec, size_t bytes_transferred) { ts = {true,bytes_transferred}; });
    sp.get_io_service().reset();
    while(!ts.isFinish && (steady_clock::now()-start < wait_time)) {
        auto newWaitTime = (milliseconds(10) > (wait_time-(steady_clock::now()-start)) ? milliseconds(10) : (wait_time-(steady_clock::now()-start)));
        sp.get_io_service().run_one_for(newWaitTime);
    }
    if(!ts.isFinish) {
        sp.cancel();
        sp.get_io_service().run_one();
    }
    this_thread::yield();
    return ts.bytes_transferred;
}
vector<string> getAllPorts()
{
    vector<string> ports;
#ifdef _WIN32
    HKEY hKey = NULL; //保存注册表的句柄
    DWORD dwIndexs = 0; //需要返回的索引
    TCHAR keyName[MAX_PATH] = { 0 }; //名称
    DWORD nameLength = MAX_PATH;  //想要读取多少字节并返回实际读取到的字符长度
    BYTE keyValue[256] = { 0 };
    DWORD valueLength = 256;
    auto type = REG_SZ;

    if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, TEXT("HARDWARE\\DEVICEMAP\\SERIALCOMM"), 0, KEY_READ, &hKey) == ERROR_SUCCESS)
    {
        while (RegEnumValueA(hKey, dwIndexs, keyName, &nameLength, NULL, &type, keyValue, &valueLength) == ERROR_SUCCESS)
        {
            ++dwIndexs;
            if(type == REG_SZ)
                ports.emplace_back((char *)keyValue);
            nameLength = MAX_PATH;
            valueLength = 256;
        }
    }
    if (hKey != NULL)
    {
        RegCloseKey(hKey);
    }
    if(ports.empty()) {
        for(int i = 1; i <= 255; i++) {
            TCHAR strPort[32] = {0};
            sprintf(strPort, "COM%d", i);

            DWORD dwSize = 0;
            LPCOMMCONFIG lpCC = (LPCOMMCONFIG) new BYTE[1];
            BOOL ret = GetDefaultCommConfig(strPort, lpCC, &dwSize);
            delete[] lpCC;

            lpCC = (LPCOMMCONFIG) new BYTE[dwSize];
            ret = GetDefaultCommConfig(strPort, lpCC, &dwSize);
            if(ret)
                ports.push_back(strPort);
            delete[] lpCC;

        }
    }
#else
    DIR *dir;
    struct dirent *ent;
    if ((dir = opendir ("/dev/")) != NULL) {
        /* print all the files and directories within directory */
        while ((ent = readdir (dir)) != NULL) {
            string name(ent->d_name);
            if(name.find("ttyUSB") != string::npos || name.find("ttyACM") != string::npos || name.find("ttyAMA") != string::npos || name.find("comm") != string::npos)
                ports.push_back("/dev/"+name);
        }
        closedir (dir);
    }
#endif
    return std::move(ports);
}

struct serialStatus {
    mutex mut;
    vector<string> connected;
};

void runArduino2tmsg(serialStatus *status,string a2t_exec, string serialport, string json, string serverURI) {
    {
        lock_guard<mutex> lock (status->mut);
        if(find(status->connected.begin(), status->connected.end(), serialport) != status->connected.end())
            return;
        status->connected.emplace_back(serialport);
    }
    string s = a2t_exec + " " + serialport + " " + json + " " + serverURI;
#ifdef _WIN32
    s += " >NUL";
#else
    s += " >/dev/null";
#endif
    cout << "Running " << s << endl;
    system(s.c_str());
    cout << "Finished " << s << endl;
    {
        lock_guard<mutex> lock (status->mut);
        auto pos = find(status->connected.begin(), status->connected.end(), serialport);
        if(pos != status->connected.end())
            status->connected.erase(pos);
    }
}

int main(int argc, char* argv[])
{
    if(argc < 2 || string(argv[1]).find('h') < 3) {//无输入, 或第一个参数为-h,--h,--help,help
        cout << "Usage: " << argv[0] << " SerialPortDefineJSON [MQTT Server URI]" << endl;
        return 0;
    }
    string serverURI;
    if(argc > 2)
        serverURI = argv[2];
    if(serverURI.empty())
        serverURI = "tcp://localhost:1883";

    io_service io_s;
    //读取配置信息
    ifstream defineJSON(argv[1]);
    auto define = json::parse(string((istreambuf_iterator<char>(defineJSON)), istreambuf_iterator<char>())).get<map<string, string>>();
    //建立缓冲区
    unsigned char cache[256] = {0};
    auto buf = buffer(cache, 256);
    auto buf2 = buffer(cache, 3);
    //读取数据转发
    serialStatus status;
    string a2t_exec(argv[0]);
    auto a2t_exec_auto = a2t_exec.rfind("_auto");
    if(a2t_exec_auto == string::npos) {
        cout << "Can't find _auto string in command." << endl;
        return -1;
    }
    a2t_exec.erase(a2t_exec_auto, 5);
    std::list<std::future<void>> futures;
    while(1) {
        cout << "Scanning..." << endl;
        auto ports = getAllPorts();
        for(auto &i : ports) {
            auto pos = find(status.connected.begin(), status.connected.end(), i);
            if(pos == status.connected.end()) {
                string defineJsonURL;
                try {
                    serial_port sp(io_s, i);
                    //设置串口参数
                    sp.set_option(serial_port::baud_rate(115200));
                    sp.set_option(serial_port::flow_control());
                    sp.set_option(serial_port::parity());
                    sp.set_option(serial_port::stop_bits());
                    sp.set_option(serial_port::character_size(8));
                    if(!sp.is_open()) {
                        cout << "Open serial port " << i << " fail." << endl;
                        continue;
                    }
                    //读取设备id
                    int tryCount = 0;
                    memset(cache,0,3);
                    while((cache[0] != 0xEE || cache[2] != 0x1D) && tryCount++ < 3) {
                        readTimeout(sp, buf, milliseconds(100));
                        write(sp, buffer(string("0"), 1));//请求id
                        if(readTimeout(sp, buf2, milliseconds(600)) == 0)//如果没有数据,再减少一次重试
                            tryCount++;
                    }
                    sp.close();
                    if(cache[0] != 0xEE || cache[1] == 0 || cache[2] != 0x1D) {
                        cout << "Can't read " << i << " device id." << endl;
                        continue;
                    }
                    unsigned int device_id = cache[1];
                    cout << i << " Device ID: " << device_id << endl;
                    for(auto &j : define) {//优先串口名匹配
                        if(j.first == i) {
                            defineJsonURL = j.second;
                            break;
                        }
                    }
                    for(auto &j : define) {
                        if(atoi(j.first.c_str()) == device_id) {//atoi转换失败是返回0,而device_id不能定义为0
                            defineJsonURL = j.second;
                            break;
                        }
                    }
                }
                catch(...) {
                    cout << "Open serial port " << i << "fail." << endl;
                }
                if(defineJsonURL.empty()) {
                    cout << "Can't find " << i << " define json." << endl;
                }
                else
                    futures.push_back(std::async(std::launch::async, runArduino2tmsg, &status, a2t_exec, i, defineJsonURL, serverURI));
            }
        }
        this_thread::sleep_for(milliseconds(2500));
        for(auto it = futures.begin(); it != futures.end();) {
            if(it->wait_for(microseconds(1)) == future_status::ready)
                it = futures.erase(it);
            else
                it++;
        }
    }
    return 0;
}